package com.nulldev.util.os;

import java.util.regex.Pattern;

import com.nulldev.util.FinalBypass;
import com.nulldev.util.io.FileUtil;
import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.java.ProcessUtil;
import com.nulldev.util.math.MathUtil;
import com.nulldev.util.os.OSUtil.OSType;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.string.StringsUtil;

/**
 * NVIDIA format: NVIDIA XXX [Brand] XXX [Model: GTX] XXX [ID] <br>
 *
 */
public class GPUUtil {

	public static final FinalBypass<String> NVIDIA_ASCII = new FinalBypass<String>("");
	public static final FinalBypass<String> RADEON_ASCII = new FinalBypass<String>("");
	public static final FinalBypass<String> AMD_ASCII = new FinalBypass<String>("");
	public static final FinalBypass<String> INTEL_ASCII = new FinalBypass<String>("");

	static {
		try {
			NVIDIA_ASCII.setObject(FileUtil.read(GPUUtil.class.getResourceAsStream("gpu/nvidia.ascart")));
			NVIDIA_ASCII.lock();
		} catch (Exception e) {
		}
		try {
			RADEON_ASCII.setObject(FileUtil.read(GPUUtil.class.getResourceAsStream("gpu/radeon.ascart")));
			RADEON_ASCII.lock();
		} catch (Exception e) {
		}
		try {
			AMD_ASCII.setObject(FileUtil.read(GPUUtil.class.getResourceAsStream("gpu/amd.ascart")));
			AMD_ASCII.lock();
		} catch (Exception e) {
		}
//		try {
//			INTEL_ASCII.setObject(FileUtil.read(GPUUtil.class.getResourceAsStream("gpu/intel.ascart")));
//			INTEL_ASCII.lock();
//		} catch (Exception e) {
//		}
	}

	public enum Vendor {
		NVIDIA("Nvidia"), RADEON("Radeon"), AMD("AMD (Radeon)"), INTEL("Intel"), SGI("SGI"), VOODOO("3DFX Voodoo"), MESA_SRI("Mesa + SRI @ GNU/Linux"), OTHER("Unknown");

		private String displayName;
		private Vendor(final String displayName) {
			this.displayName = displayName;
		}

		@Override
		public String toString() {
			return this.displayName;
		}

		public String displayName() {
			return this.displayName;
		}

		public String vendor_ascii() {
			if (this.displayName == "Nvidia") {
				return NVIDIA_ASCII.getObject();
			}else if (this.displayName == "AMD (Radeon)") {
				return AMD_ASCII.getObject();
			}else if (this.displayName == "Radeon") {
				return RADEON_ASCII.getObject();
			}else if (this.displayName == "Intel") {
				return INTEL_ASCII.getObject();
			}else {
				return "";
			}
		}
	}

	public enum Brands {
		/*
		 * NVIDIA
		 */
		GEFORCE_GTX("Geforce GTX"), GEFORCE("Geforce"), GEFORCE_OLD("Geforce (pre-GT)"), TESLA("Tesla"),
		/*
		 * AMD
		 */
		Radeon_RX("Radeon RX"), Radeon_RX_Vega("Radeon RX Vega"), AMD_R9("AMD Radeon R9"), AMD_R7("AMD Radeon R7"), AMD_R5("AMD Radeon R5"), AMD_R3("AMD Radeon R3"), AMD_APU("AMD Radeon HD 8xxx [APU Only]"), AMD_HD_7XXX("AMD Radeon HD 7xxx"), AMD_HD_6XXX("AMD Radeon HD 6xxx"), AMD_HD_5XXX("AMD Radeon HD 5xxx"), AMD_HD_4XXX("AMD Radeon HD 4xxx"), AMD_HD_3XXX("AMD Radeon HD 3xxx"), AMD_HD_2XXX("AMD Radeon HD 2xxx"), AMD_HD_1XXX("AMD Radeon XT 1xxx"), AMD_XT("AMD Radeon XT"),
		/*
		 * Others
		 */
		INTEL_HD_6XX("Intel HD Graphics (6XX)"),
		INTEL_HD_5XX("Intel HD Graphics (5XX)"),
		INTEL_HD_4XX("Intel HD Graphics (4XX)"),
		UNKNOWN("Unknown");

		private String displayName;
		private Brands(final String displayName) {
			this.displayName = displayName;
		}

		@Override
		public String toString() {
			return this.displayName;
		}

		public String displayName() {
			return this.displayName;
		}

		public String displaySeries(final char series) {
			return this.displayName + " " + series + "xx Series";
		}

		public static Brands byName(final String name) {
			for (final Brands br : values()) {
				if (br.name().equals(name)) {
					return br;
				}
			}
			return UNKNOWN;
		}
	}

	public static class GPU{
		private Vendor vendor;
		private String model;
		private String[] data;

		public GPU(final Vendor vendor) {
			this.vendor = vendor;
			this.model = "Unknown Model by N/A";
			this.data = new String[] {"Unknown", "N/A", "N/A", "N/A"};
		}

		public GPU(final Vendor vendor, final String model) {
			this.vendor = vendor;
			this.model = model;
			this.data = model.split(" ");
		}

		public Vendor getVendor() {
			return this.vendor;
		}

		public String getGPU() {
			return StringsUtil.filterStringS(this.model, 1);
		}

		public Brands getBrand() {
			try {
				if (this.vendor == Vendor.NVIDIA) {
					try {
						if (this.data[1].equals("GeForce") && this.data[2].equals("GTX")) {
							return Brands.GEFORCE_GTX;
						} else if (this.data[1].equals("GeForce") && MathUtil.stringBetweenList(this.data, 2, 1000, 9800)) {
							return Brands.GEFORCE;
						}else if (this.data[1].equals("GeForce")){
							return Brands.GEFORCE_OLD;
						}
					}catch (Exception e) {
						if (this.data[1].equals("GeForce") && MathUtil.stringBetweenList(this.data, 2, 1000, 9800)) {
							return Brands.GEFORCE;
						}else if (this.data[1].equals("GeForce")){
							return Brands.GEFORCE_OLD;
						}
					}
				}else if (this.vendor == Vendor.RADEON) {
					final boolean isRX = StringsUtil.isEqual(this.data, 1, "RX");
					final boolean isVega = StringsUtil.isEqual(this.data, 2, "Vega");
					if (isRX && !isVega) {
						return Brands.Radeon_RX;
					}else if (isRX && isVega) {
						return Brands.Radeon_RX_Vega;
					}
				}else if (this.vendor == Vendor.AMD) {
					final boolean isR9 = StringsUtil.isEqual(this.data, 2, "R9");
					final boolean isR5 = StringsUtil.isEqual(this.data, 2, "R5");
					final boolean isR7 = StringsUtil.isEqual(this.data, 2, "R7");
					final boolean isR3 = StringsUtil.isEqual(this.data, 2, "R3");
					
					final boolean isHD8xxx = StringsUtil.isEqual(this.data, 7, "HD") && MathUtil.stringBetweenListRC(this.data, 8, 8000, 8999);
					final boolean isHD7xxx = StringsUtil.isEqual(this.data, 7, "HD") && MathUtil.stringBetweenListRC(this.data, 8, 7000, 7999);
					final boolean isHD6xxx = StringsUtil.isEqual(this.data, 7, "HD") && MathUtil.stringBetweenListRC(this.data, 8, 6000, 6999);
					final boolean isHD5xxx = StringsUtil.isEqual(this.data, 7, "HD") && MathUtil.stringBetweenListRC(this.data, 8, 5000, 5999);
					final boolean isHD4xxx = StringsUtil.isEqual(this.data, 7, "HD") && MathUtil.stringBetweenListRC(this.data, 8, 4000, 4999);
					final boolean isHD3xxx = StringsUtil.isEqual(this.data, 7, "HD") && MathUtil.stringBetweenListRC(this.data, 8, 3000, 3999);
					final boolean isHD2xxx = StringsUtil.isEqual(this.data, 7, "HD") && MathUtil.stringBetweenListRC(this.data, 8, 2000, 2999);
					final FinalBypass<Boolean> isXT = new FinalBypass<Boolean>(false);
					final FinalBypass<Boolean> isXT1xxx = new FinalBypass<Boolean>(false);
					try {
						final String out = this.data[2].substring(1);
						final int range = Integer.parseInt(out);
						isXT.setObject(range < 1000 && range > 0 && this.model.contains("XT"));
					}catch (Exception e) {}
					try {
						final int range = Integer.parseInt(this.data[2].substring(1));
						isXT1xxx.setObject(range < 2000 && range > 1000);
					}catch (Exception e) {}
					if (isR9) {
						return Brands.AMD_R9;
					}else if (isR7) {
						return Brands.AMD_R7;
					}else if (isR5) {
						return Brands.AMD_R5;
					}else if (isR3) {
						return Brands.AMD_R3;
					}else if (isHD8xxx) {
						return Brands.AMD_APU;
					}else if (isHD7xxx) {
						return Brands.AMD_HD_7XXX;
					}else if (isHD6xxx) {
						return Brands.AMD_HD_6XXX;
					}else if (isHD5xxx) {
						return Brands.AMD_HD_5XXX;
					}else if (isHD4xxx) {
						return Brands.AMD_HD_4XXX;
					}else if (isHD3xxx) {
						return Brands.AMD_HD_3XXX;
					}else if (isHD2xxx) {
						return Brands.AMD_HD_2XXX;
					}else if (isXT1xxx.getObject()) {
						return Brands.AMD_HD_1XXX;
					}else if (isXT.getObject()) {
						return Brands.AMD_XT;
					}else if (this.vendor == Vendor.AMD && OSUtil.getOperatingSystemType() == OSType.Linux) {
						final String[] dta = StringsUtil.substringBetween(ArrayUtils.StringArrayUtils.addFrom(this.data, 0).replaceAll(Pattern.quote("[AMD/ATI]"), ""), "[", "]").split(" ");
						if (dta[0].equals("Radeon") && dta[1].equals("RX")) {
							try {
								if (dta[2].equals("Vega")) {
									return Brands.Radeon_RX_Vega;
								}else {
									return Brands.Radeon_RX;
								}
							}catch (Exception e) {
								return Brands.Radeon_RX;
							}
						}
					}
				}else if (this.vendor == Vendor.INTEL) {
					final boolean is6xx = MathUtil.stringBetweenList(this.data, 2, 600, 699);
					if (is6xx) {
						return Brands.INTEL_HD_6XX;
					}
				}
			}catch (Exception e) {
				return Brands.UNKNOWN;
			}
			return Brands.UNKNOWN;
		}

		@Deprecated
		public String filteredModel() {
			if (this.vendor == Vendor.NVIDIA) {
				final boolean isTi = StringsUtil.isEqual(this.data, 4, "Ti");
				if (isTi) {

				}
			}
			return this.model;
		}

		@Override
		public String toString() {
			final StringBuilder out = new StringBuilder();
			out.append("GPU["
					+ "gpu="
					+ (OSUtil.getOperatingSystemType() == OSType.Linux ? 
							ArrayUtils.StringArrayUtils.addFrom(this.data, 0) :
								ArrayUtils.StringArrayUtils.addFrom(this.data, 1))
					+ ",vendor=" + this.vendor + ",brand=" + this.getBrand().displayName() + "]");
//			out.append("GPUUtil.GPU{");
//			if (OSUtil.getOperatingSystemType() == OSType.Linux) {
//				out.append(ArrayUtils.StringArrayUtils.addFrom(this.data, 0));
//			}else {
//				out.append(ArrayUtils.StringArrayUtils.addFrom(this.data, 1));
//			}
//			out.append(" by ");
//			out.append(this.vendor + " | ");
//			out.append("brand= ");
//			if (this.getBrand() == Brands.GEFORCE_GTX) {
//				out.append(this.getBrand().displaySeries(this.data[3].charAt(0)));
//			}else {
//				out.append(this.getBrand());
//			}
//			out.append("}");
			return out.toString();//"GPU[" + ArrayUtils.StringArrayUtils.addFrom(this.data, 1) + " by " + this.vendor + ". brand=" + this.getBrand() + "]";
		}

		public String[] getData() {
			return this.data;
		}

		public void println() {
			synchronized (System.err) {
				synchronized (System.out) {
					System.out.println(this.toString());
				}
			}
		}

		public void dx_report() {
			for (int i = 1; i < 13; i++) {
				synchronized (System.err) {
					synchronized (System.out) {
						System.out.println("DX" + i + " support >> " + GPUUtil.isD3DReady(this, i));
					}
				}
			}
		}
	}

	public static GPU parse() {
		try {
			final String data = gpuName();
			final Vendor parseVendor = parseVendor(data);
			return new GPU(parseVendor, data);
		}catch (Exception e) {
			throw new RuntimeException(JVMUtil.fromError(e));
		}
	}

	public static Vendor parseVendor(final String data) {
		final String[] items = data.split(" ");
		try {
			if (items[0].equals("Advanced") && items[1].equals("Micro") && items[2].equals("Devices,") && items[3].equals("Inc.")) {
				return Vendor.AMD;
			}
		}catch (Exception e) {
		}
		if (items[0].equals("NVIDIA")) {
			return Vendor.NVIDIA;
		}else if (items[0].equals("RADEON(tm)") || items[0].equals("Radeon(tm)") || items[0].equals("Radeon(TM)")) {
			return Vendor.RADEON;
		}else if (items[0].equals("AMD(tm)")) {
			return Vendor.AMD;
		}else if (items[0].equals("Intel(tm)")) {
			return Vendor.INTEL;
		}else if (data.contains("Intel Corporation")) {
			return Vendor.INTEL;
		}
		return Vendor.OTHER;
	}

	public static Vendor getVendor() {
		try {
			return parseVendor(gpuName());
		} catch (Exception e) {
			e.printStackTrace();
			return Vendor.OTHER;
		}
	}

	public static String gpuName() throws Exception {
		if (OSUtil.getOperatingSystemType() == OSType.Windows) {
			final String out = ProcessUtil.execCmd("wmic path win32_VideoController get name");
			if (out.contains("NVIDIA")) {
				final String cleared = StringsUtil.filterStringS(out.substring(out.indexOf("NVIDIA")), 1);
				return cleared.substring(0, cleared.length() - 1);
				//return "Radeon(tm) RX 460";//cleared.substring(0, cleared.length() - 1);
			}else if (out.contains("Radeon(TM)")) {
				final String cleared = StringsUtil.filterStringS(out.substring(out.indexOf("Radeon(TM)")), 1);
				return out.substring(out.indexOf("Radeon(TM)"), out.length() - 1);
			}else {
				return "N/A";
			}
		}else if (OSUtil.getOperatingSystemType() == OSType.Linux) {
			final String[] out = StringsUtil.byLineSepeartor(ProcessUtil.execCmd("lspci"));
			final String investigate = ArrayUtils.StringArrayUtils.find(out, "VGA compatible controller: ");
			try {
				final String[] gpuData = investigate.split("VGA compatible controller: ");
				final String filtered = gpuData[1];
				return filtered;
			}catch (Exception e) {
				System.err.println("[GPUUtil] An error occured while running 'lspci'.");
			}
		}
		System.err.println("[GPUUtil] Unsupported OS!");
		return "N/A";//throw new UnsupportedOperationException("Unsupported OS");
	}

	public static GPU fake(final Brands brand) {
		if (brand == Brands.GEFORCE || brand == Brands.GEFORCE_GTX || brand == Brands.GEFORCE_OLD) {
			return new GPU(Vendor.NVIDIA, brand.toString());
		}else if (brand == Brands.AMD_HD_2XXX || brand == Brands.AMD_HD_1XXX || brand == Brands.AMD_HD_3XXX || brand == Brands.AMD_HD_4XXX || brand == Brands.AMD_HD_5XXX || brand == Brands.AMD_HD_6XXX || brand == Brands.AMD_HD_7XXX || brand == Brands.AMD_R3 || brand == Brands.AMD_R5 || brand == Brands.AMD_R7 || brand == Brands.AMD_R9) {
			return new GPU(Vendor.AMD, brand.toString());
		}else if (brand == Brands.Radeon_RX || brand == Brands.Radeon_RX_Vega) {
			return new GPU(Vendor.RADEON, brand.toString());
		}
		return new GPU(Vendor.OTHER, brand.toString());
	}

	public static boolean isD3DReady(final GPU gpu, final int level) {
		final int dx = (int) MathUtil.clamp(1, 12, level);
		final Brands[] D3D_12 = {Brands.AMD_HD_7XXX, Brands.Radeon_RX, Brands.Radeon_RX_Vega, Brands.AMD_R3, Brands.AMD_R5, Brands.AMD_R7, Brands.AMD_R9, Brands.GEFORCE_GTX, Brands.TESLA};
		final Brands[] D3D_11 = {Brands.AMD_HD_7XXX, Brands.AMD_HD_6XXX, Brands.AMD_HD_5XXX, Brands.AMD_HD_4XXX, Brands.Radeon_RX, Brands.Radeon_RX_Vega, Brands.AMD_R3, Brands.AMD_R5, Brands.AMD_R7, Brands.AMD_R9, Brands.GEFORCE_GTX, Brands.INTEL_HD_4XX, Brands.INTEL_HD_5XX, Brands.INTEL_HD_6XX, Brands.TESLA, Brands.AMD_APU};
		final Brands[] D3D_10 = {Brands.AMD_HD_7XXX, Brands.AMD_HD_6XXX, Brands.AMD_HD_5XXX, Brands.AMD_HD_4XXX, Brands.AMD_HD_3XXX, Brands.AMD_HD_2XXX, Brands.Radeon_RX, Brands.Radeon_RX_Vega, Brands.AMD_R3, Brands.AMD_R5, Brands.AMD_R7, Brands.AMD_R9, Brands.GEFORCE_GTX, Brands.GEFORCE, Brands.INTEL_HD_4XX, Brands.INTEL_HD_5XX, Brands.INTEL_HD_6XX, Brands.TESLA};
		final Brands[] D3D_9 = {Brands.AMD_HD_7XXX, Brands.AMD_HD_6XXX, Brands.AMD_HD_5XXX, Brands.AMD_HD_4XXX, Brands.AMD_HD_3XXX, Brands.AMD_HD_2XXX, Brands.AMD_HD_1XXX, Brands.Radeon_RX, Brands.Radeon_RX_Vega, Brands.AMD_R3, Brands.AMD_R5, Brands.AMD_R7, Brands.AMD_R9, Brands.GEFORCE_GTX, Brands.GEFORCE, Brands.INTEL_HD_4XX, Brands.INTEL_HD_5XX, Brands.INTEL_HD_6XX, Brands.TESLA};
		final Brands[] D3D_8 = D3D_9;
		final Brands[] D3D_7 = ArrayUtils.addAll(D3D_8, new Brands[] {Brands.GEFORCE_OLD, Brands.AMD_XT});
		final Brands[] D3D_6 = D3D_7;
		final Brands[] D3D_5 = D3D_6;
		final Brands[] D3D_4 = D3D_5;
		final Brands[] D3D_3 = D3D_4;
		final Brands[] D3D_2 = D3D_3;
		final Brands[] D3D_1 = D3D_2;

		if (dx == 12) {
			if (gpu.getVendor() == Vendor.NVIDIA && gpu.getBrand() == Brands.GEFORCE_GTX) {
				if (StringsUtil.isEqual(gpu.getData(), 3, "770")) {
					return false;
				}else {
					return ArrayUtils.contains(D3D_12, gpu.getBrand());
				}
			}else {
				return ArrayUtils.contains(D3D_12, gpu.getBrand());
			}
		}else if (dx == 11) {
			return ArrayUtils.contains(D3D_11, gpu.getBrand());
		}else if (dx == 10) {
			return ArrayUtils.contains(D3D_10, gpu.getBrand());
		}else if (dx == 9) {
			return ArrayUtils.contains(D3D_9, gpu.getBrand());
		}else if (dx == 8) {
			return ArrayUtils.contains(D3D_8, gpu.getBrand());
		}else if (dx == 7) {
			return ArrayUtils.contains(D3D_7, gpu.getBrand());
		}else if (dx == 6) {
			return ArrayUtils.contains(D3D_6, gpu.getBrand());
		}else if (dx == 5) {
			return ArrayUtils.contains(D3D_5, gpu.getBrand());
		}else if (dx == 4) {
			return ArrayUtils.contains(D3D_4, gpu.getBrand());
		}else if (dx == 3) {
			return ArrayUtils.contains(D3D_3, gpu.getBrand());
		}else if (dx == 2) {
			return ArrayUtils.contains(D3D_2, gpu.getBrand());
		}else if (dx == 1) {
			return ArrayUtils.contains(D3D_1, gpu.getBrand());
		}
		return false;
	}
}
